package cn.learn.redis;


import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

/*
 * 1、StringRedisTemplate继承RedisTemplate
 * 2、两者的数据是不共通的；也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据，RedisTemplate只能管理RedisTemplate中的数据。
 * 3、序列化策略有两种，一种是String的序列化策略，一种是JDK的序列化策略。
 *   StringRedisTemplate默认采用的是String的序列化策略，保存的key和value都是采用此策略序列化保存的。
 *   RedisTemplate默认采用的是JDK的序列化策略，保存的key和value都是采用此策略序列化保存的。
 * 网友结论：
 *   当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候，那么你就使用StringRedisTemplate即可，
 *   但是如果你的数据是复杂的对象类型，而取出的时候又不想做任何的数据转换，直接从Redis里面取出一个对象，那么使用RedisTemplate是更好的选择
 * ==============================================================================================================================================================================
 * 最后结论：选择spring默认推荐的StringRedisTemplate，JDK的序列化占用内存很大，时间也要更多。耗时、内存很重要，所以手动做数据转换还是值得的
 *
 */

/**
 * redis常用的方法工具类【初步方法，还要测试不同场景下的情况，有数据没数据情况下会不会报错】
 *
 * @author huangyezhan
 * @version 1.0
 * @date 2020年01月07日23:33
 */
@Component
public class RedisUtil {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * ==========================================================================================================================================================================
     * ============================================================================= string数据类型 ==============================================================================
     * 有三种类型：字符串，整数，浮点。
     * 1. 数据缓存：高并发数据，可以从数据库拿出来存在这里
     * 2. 数据统计：用来做记录，eg：整型可以用来记录网站访问量，某个文件的下载量。（自增自减）
     * 3. 时间内限制请求次数：比如已登录用户请求短信验证码，验证码在5分钟内有效的场景。
     *    eg：当用户首次请求了短信接口，将用户id存储到redis 已经发送短信的字符串中，并且设置过期时间为5分钟。当该用户再次请求短信接口，发现已经存在该用户发送短信记录，则不再发送短信。
     * 4. 分布式session
     * ==========================================================================================================================================================================
     */

    /**
     * 写入redis，string数据类型，永久
     *
     * @param key   写入的key
     * @param value 存储的数据
     * @return 插入成功true，否则false
     */
    public boolean saveValue(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
            String                          jsonString  = JSONObject.toJSONString(value);
            opsForValue.set(key, jsonString);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 写入redis，string数据类型，有过期时间
     *
     * @param key        写入的key
     * @param value      存储的数据
     * @param expireTime 有效时间，单位是（秒s）
     * @return 插入成功true，否则false
     */
    public boolean saveValue(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
            String                          jsonString  = JSONObject.toJSONString(value);
            opsForValue.set(key, jsonString, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 如果不存在则插入，返回true为插入成功,false失败
     *
     * @param key   key
     * @param value value
     * @return 插入成功true，否则false
     */
    public Boolean saveIfAbsentValue(final String key, Object value) {
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        String                          jsonString  = JSONObject.toJSONString(value);
        return opsForValue.setIfAbsent(key, jsonString);
    }

    /**
     * 如果不存在则插入，返回true为插入成功,false失败
     *
     * @param key        key
     * @param value      value
     * @param expireTime 过期时长
     * @return 插入成功true，否则false
     */
    public Boolean saveIfAbsentValue(final String key, Object value, Long expireTime) {
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        String                          jsonString  = JSONObject.toJSONString(value);
        return opsForValue.setIfAbsent(key, jsonString, expireTime, TimeUnit.SECONDS);
    }

    /**
     * 批量插入，有相同的key会被覆盖掉
     *
     * @param map 按照键值对插入
     */
    public void saveBatchValue(final Map<String, String> map) {
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        opsForValue.multiSet(map);
    }

    /**
     * 批量插入，如果有一个key一样的，全部不插入
     *
     * @param map 按照键值对插入
     * @return 插入成功返回true，否则false
     */
    public Boolean saveBatchIfAbsentValue(final Map<String, String> map) {
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        return opsForValue.multiSetIfAbsent(map);
    }

    /**
     * 根据key删除数据
     *
     * @param key key
     * @return 正常删除返回true，否则false
     */
    public Boolean deleteValue(final String key) {
        RedisOperations<String, String> operations = stringRedisTemplate.opsForValue().getOperations();
        return operations.delete(key);
    }

    /**
     * 批量删除数据
     *
     * @param keys key数组
     * @return 正常删除返回true，否则false
     */
    public Long deleteBatchValue(final String... keys) {
        RedisOperations<String, String> operations = stringRedisTemplate.opsForValue().getOperations();
        List<String>                    strings    = Arrays.asList(keys);
        Long                            delete     = operations.delete(strings);
        return delete;
    }

    /**
     * 获取指定key的值
     *
     * @param key redis上的key
     * @return 返回字符串数值
     */
    public String getValue(final String key) {
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        return opsForValue.get(key);
    }

    /**
     * 获取指定key的值
     *
     * @param key redis上的key
     * @param clz 指定数据类型
     * @param <T> 泛型
     * @return 把字符串强转成指定的数据类型clz
     */
    public <T> T getValue(final String key, Class<T> clz) {
        String value = this.getValue(key);
        T      t     = value == null ? null : JSONObject.parseObject(value, clz);
        return t;
    }

    /**
     * 批量获取指定key的值
     *
     * @param keys key数组
     * @return 返回集合
     */
    public List<String> getBatchValue(final String... keys) {
        List<String> listKey = Arrays.asList(keys);
        return this.getBatchValue(listKey);
    }

    /**
     * 批量获取指定key的值
     *
     * @param keys key的集合
     * @return 返回集合
     */
    public List<String> getBatchValue(final Collection<String> keys) {
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        return opsForValue.multiGet(keys);
    }

    /**
     * 批量获取指定key的值，并强转为指定的数据类型，所以集合的数据类型必须都是一样的
     *
     * @param clz  需要强转的数据类型
     * @param keys key数组
     * @param <T>  泛型
     * @return 返回强转后数据类型后的集合
     */
    public <T> List<T> getBatchValue(Class<T> clz, String... keys) {
        List<T> list = this.getBatchValue(clz, Arrays.asList(keys));
        return list;
    }

    /**
     * 批量获取指定key的值，并强转为指定的数据类型，所以集合的数据类型必须都是一样的
     *
     * @param clz  需要强转的数据类型
     * @param keys key集合
     * @param <T>  泛型
     * @return 返回强转后数据类型后的集合
     */
    public <T> List<T> getBatchValue(Class<T> clz, final Collection<String> keys) {
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        List<String>                    strings     = opsForValue.multiGet(keys);
        List<T>                         list        = new ArrayList<>();
        for (String s : strings) {
            T t = JSONObject.parseObject(s, clz);
            list.add(t);
        }
        return list;
    }

    /**
     * 在指定key后追加数值
     *
     * @param key   key
     * @param value 追加的数
     * @return 返回
     */
    public Integer appendValue(String key, String value) {
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        return opsForValue.append(key, value);
    }

    /**
     * 获取原来key键对应的值并重新赋新值
     *
     * @param key   key
     * @param value 新的value
     */
    public void getAndSet(final String key, String value) {
        String set = stringRedisTemplate.opsForValue().getAndSet(key, value);
    }

    /**
     * 获取指定key的值进行减1，如果value不是integer类型，会抛异常，如果key不存在会创建一个，默认value为0
     *
     * @param key key
     * @return
     */
    public Long decrement(final String key) {
        Long decrement = stringRedisTemplate.opsForValue().decrement(key);
        return decrement;
    }

    /**
     * @param key   key
     * @param delta 减少的量delta
     * @return
     */
    public Long decrement(final String key, long delta) {
        Long decrement = stringRedisTemplate.opsForValue().decrement(key, delta);
        return decrement;
    }

    /**
     * 获取指定key的值进行加1，如果value不是integer类型，会抛异常，如果key不存在会创建一个，默认value为0
     *
     * @param key key
     * @return
     */
    public Long increment(final String key) {
        Long increment = stringRedisTemplate.opsForValue().increment(key);
        return increment;
    }

    /**
     * @param key   key
     * @param delta 增加的量delta
     * @return
     */
    public Long increment(final String key, long delta) {
        Long increment = stringRedisTemplate.opsForValue().increment(key, delta);
        return increment;
    }


    /**
     * ==========================================================================================================================================================================
     * ============================================================================== hash数据类型 ===============================================================================
     * hash类型用来存储对象最佳
     * 在实际的开发中为了不让Redis占用内存较大，所以在使用Redis哈希类型的时候，尽量不要创建过多的键。
     * 我们知道Redis哈希类型的内部编码有ziplist和hashtable两种，并且在符合某些条件的情况下，这两种编码会自动转换。
     * 当Redis哈希类型的内部编码变成hashtable时，会比ziplist更消耗内存，在这一点要特别注意。
     * ==========================================================================================================================================================================
     */

    /**
     * 写入redis，hash类型的数据
     *
     * @param redisKey 存在redis里面的key
     * @param hashKey  属性的key，现在这里放对象的key
     * @param o        属性的值，现在这里直接就存放整个对象
     */
    public void saveHash(final String redisKey, final String hashKey, Object o) {
        HashOperations opsForHash = stringRedisTemplate.opsForHash();
        String         jsonString = JSONObject.toJSONString(o);
        opsForHash.put(redisKey, hashKey, jsonString);
    }

    /**
     * 指定key获取对应的对象信息
     *
     * @param redisKey 存在redis里面的key
     * @param hashKey  属性的key，现在这里放对象的key
     * @return 返回查询出来的对象
     */
    public Object getHash(final String redisKey, final String hashKey) {
        HashOperations opsForHash = stringRedisTemplate.opsForHash();
        Object         o          = opsForHash.get(redisKey, hashKey);
        return o;
    }

    /**
     * 指定key获取对应的对象信息
     *
     * @param redisKey 存在redis里面的key
     * @param hashKey  属性的key，现在这里放对象的key
     * @param clz      指定数据类型
     * @param <T>      泛型
     * @return 返回泛型指定的数据类型对象
     */
    public <T> T getHash(final String redisKey, final String hashKey, Class<T> clz) {
        String o = (String) this.getHash(redisKey, hashKey);
        T      t = o == null ? null : JSONObject.parseObject(o, clz);
        return t;
    }

    /**
     * 查询看是否有指定的hash对象
     *
     * @param redisKey 存在redis里面的key
     * @param hashKey  属性的key，现在这里放对象的key
     * @return true为有改对象
     */
    public Boolean existsHashKey(final String redisKey, final String hashKey) {
        HashOperations opsForHash = stringRedisTemplate.opsForHash();
        Boolean        hasKey     = opsForHash.hasKey(redisKey, hashKey);
        return hasKey;
    }

    /**
     * 删除hash对象
     *
     * @param redisKey 存在redis里面的key
     * @param hashKey  属性的key，现在这里放对象的key
     * @return 返回删除数据的条数
     */
    public Long deleteHash(final String redisKey, String... hashKey) {
        HashOperations opsForHash = stringRedisTemplate.opsForHash();
        Long           delete     = opsForHash.delete(redisKey, hashKey);
        return delete;
    }


    /**
     * ==========================================================================================================================================================================
     * =============================================================================== list数据类型 ==============================================================================
     * 应用场景：
     * 1. 消息队列
     *   list类型的lpop和rpush（或者反过来，lpush和rpop）能实现队列的功能，故而可以用Redis的list类型实现简单的点对点的消息队列。
     *   在实战中可以使用Kafka、NSQ、RabbitMQ等成熟的消息队列；听说有项目经理用过redis的消息队列才能解决商品订单的高并发，具体情况要测试比较
     * 2. 排行榜
     *   list类型的lrange命令可以分页查看队列中的数据。可将每隔一段时间计算一次的排行榜存储在list类型中，如京东每日的手机销量排行、学校每次月考学生的成绩排名、斗鱼年终盛典主播排名等
     *   但是，并不是所有的排行榜都能用list类型实现，只有定时计算的排行榜才适合使用list类型存储，与定时计算的排行榜相对应的是实时计算的排行榜，
     *   list类型不能支持实时计算的排行榜，之后在介绍有序集合sorted set的应用场景时会详细介绍实时计算的排行榜的实现。
     * 3. 最新列表
     *   list类型的lpush命令和lrange命令能实现最新列表的功能，每次通过lpush命令往列表里插入新的元素，然后通过lrange命令读取最新的元素列表，如朋友圈的点赞列表、评论列表
     *   但是，并不是所有的最新列表都能用list类型实现，因为对于频繁更新的列表，list类型的分页可能导致列表元素重复或漏掉。
     *   eg：举个例子，当前列表里由表头到表尾依次有（E，D，C，B，A）五个元素，每页获取3个元素，用户第一次获取到（E，D，C）三个元素，然后表头新增了一个元素F，列表变成了（F，E，D，C，B，A），
     *      此时用户取第二页拿到（C，B，A），元素C重复了。只有不需要分页（比如每次都只取列表的前5个元素）或者更新频率低（比如每天凌晨更新一次）的列表才适合用list类型实现。对于需要分页并且会频繁更新的列表，
     *      需用使用有序集合sorted set类型实现。另外，需要通过时间范围查找的最新列表，list类型也实现不了，也需要通过有序集合sorted set类型实现，如以成交时间范围作为条件来查询的订单列表。
     * 思考：
     *   那么问题来了，对于排行榜和最新列表两种应用场景，list类型能做到的sorted set类型都能做到，list类型做不到的sorted set类型也能做到，那为什么还要使用list类型去实现排行榜或最新列表呢，
     *   直接用sorted set类型不是更好吗？原因是sorted set类型占用的内存容量是list类型的数倍之多（之后会在容量章节详细介绍），对于列表数量不多的情况，可以用sorted set类型来实现，
     *   比如上文中举例的打擂金曲排行榜，每天全国只有一份，两种数据类型的内存容量差距可以忽略不计，但是如果要实现某首歌曲的翻唱作品地区排行榜，数百万的歌曲，300多个地区，会产生数量庞大的榜单，
     *   或者数量更加庞大的朋友圈点赞列表，就需要慎重地考虑容量的问题了。
     * ==========================================================================================================================================================================
     */

    /**
     * 在变量左边添加元素值。如果key不存在会新建，添加成功返回添加后的总个数
     *
     * @param key   key
     * @param value value
     * @return
     */
    public Long leftPush(String key, String value) {
        ListOperations<String, String> opsForList = stringRedisTemplate.opsForList();
        return opsForList.leftPush(key, value);
    }

    /**
     * 移除集合中的左边第一个元素。返回删除的元素，如果元素为空，该集合会自动删除
     *
     * @param key 集合名称
     * @return
     */
    public String rightPop(String key) {
        ListOperations<String, String> opsForList = stringRedisTemplate.opsForList();
        return opsForList.rightPop(key);
    }

    /**
     * 移除集合中左边的元素在等待的时间里，如果超过等待的时间仍没有元素则退出。
     *
     * @param key      集合名称
     * @param waitTime 等待时间
     */
    public String rightPopWait(String key, long waitTime) {
        String pop = stringRedisTemplate.opsForList().rightPop(key, waitTime, TimeUnit.SECONDS);
        return pop;
    }

    /**
     * 获取指定区间的值，从左往右开始。
     *
     * @param key   key
     * @param start 开始索引
     * @param end   结束索引，如果为-1代表整个集合
     * @return
     */
    public List<String> lrange(String key, long start, long end) {
        List<String> list = stringRedisTemplate.opsForList().range(key, start, end);
        return list;
    }

    /**
     * 从集合中删除值等于value的元素，删除个数为count的绝对值
     * count > 0：删除等于从左到右移动的值的count个元素；
     * count < 0：删除等于从右到左移动的值的count个元素；
     * count = 0：删除等于value的所有元素
     *
     * @param key   集合的key
     * @param value 值
     * @param count 元素下标索引
     * @return 返回删除元素的个数
     */
    public Long removeList(String key, String value, long count) {
        return stringRedisTemplate.opsForList().remove(key, count, value);
    }

    /**
     * 删除整个集合
     *
     * @param key 集合的key
     * @return 成功就返回true，否则false
     */
    public Boolean deleteList(String key) {
        return stringRedisTemplate.opsForList().getOperations().delete(key);
    }


}
